package tecpointbenchmark.model.modules;

// This imports the methods and constants from GL11
// They are used in the rendering
import static org.lwjgl.opengl.GL11.*;

import java.util.ArrayList;
import org.lwjgl.LWJGLException;
import org.lwjgl.Sys;
import org.lwjgl.input.Keyboard;
import org.lwjgl.input.Mouse;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.util.glu.GLU;

import tecpointbenchmark.model.dto.ResultDTO;

/**
 * This is the 3D LWJGL module.
 * @author Fredrik Gullberg
 */
public class GFX3DModuleCore {

    // DEBUGGING
    private static final boolean DEBUG = tecpointbenchmark.startup.Main.debug;

    // VSYNC ENABLED/DISABLED
    private static final boolean SYNC = false;

    // GENERAL
    private static GFX3DModuleShell SHELL_MODULE;
    private static final String GAME_TITLE = "Tecpoint 3D Graphics Benchmark";
    private static final double SCORE_FACTOR = 1;

    // SCREEN
    private static final boolean FULLSCREEN = false;
    private static final int DISPLAY_HEIGHT = 480;
    private static final int DISPLAY_WIDTH = 640;

    // FPS-MONITORING
    private static ArrayList<Long> ACTUAL_FPS = new ArrayList<Long>();
    private static long frameCounter = 0;
    private static double lastTime = getTime();

    // SYNCHRONIZING VAR
    private static final int FRAME_RATE = 60;

    // IS CHANGED BY logic()
    private static boolean finished = false;


    // GRAPHICAL OBJECT
    private float rquad = 1f;

    public GFX3DModuleCore(GFX3DModuleShell shell) {
        SHELL_MODULE = shell;
        finished = !SHELL_MODULE.getContinueBenchmarking();
    }

    /**
     * Initializes and creates the components
     * @throws LWJGLException
     */
    public void create() throws LWJGLException  {

        // ******* SETTING DISPLAY SETTINGS *******
        // Set the title
        Display.setTitle(GAME_TITLE);
        // Set fullscreen
        Display.setFullscreen(FULLSCREEN);
        // Set display mode
        DisplayMode dispMod = new DisplayMode(DISPLAY_WIDTH,DISPLAY_HEIGHT);
        Display.setDisplayMode(dispMod);

        // OPTIONAL
        if(SYNC) {
            // Set VSYNC
            Display.setVSyncEnabled(true);
        }
        
        // ******* END OF DISPLAY SETTINGS *******

        // ******* SETTING KEYBOARD AND MOUSE SETTINGS *******
        // ******* END OF KEYBOARD AND MOUSE SETTINGS *******
        
        // Display must be "created" before we "create" the keyboard and mouse
        Display.create();
        Keyboard.create();
        Mouse.create();

        // Initialize the OpenGL settings
        initgl();
    }

    /**
     * Destroys display, keyboard and mouse
     */
    public ResultDTO destroy(ResultDTO r) {
        Keyboard.destroy();
        Mouse.destroy();
        Display.destroy();
        
        long averageFps = calcScore();
        
        r.setMessage("Average FPS: " + averageFps + " FPS");
        r.setPoints((long) (averageFps*SCORE_FACTOR));
        return r;
    }

    /**
     * Private method that calculates the score of the benchmark
     * Should be called in destroy()
     * @return A string to insert into a ResultDTO message
     */
    private long calcScore() {

        // Var for total sum of frames
        long sum = 0;

        // Summarize the frames
        for(Long FPS : ACTUAL_FPS) {
            // sum = sum + FPS
            sum+=FPS;
        }

        return sum/ACTUAL_FPS.size();
    }

    /**
     * Run the game
     * @throws InterruptedException
     */
    public void run() throws InterruptedException {

        // As long as the boolean var finished is false, keep playing.
        // The code in the loop is run once per frame.
        while(!finished) {

            // Always call Window.update(), all the time - it does some behind the
            // scenes work, and also displays the rendered output
            Display.update();

            // Check if the OS or USER has requested the window closed.
            if(Display.isCloseRequested()) {
                finished = true;
            }

            // If NOT, check if the window is active and if so...
            else if (Display.isActive()) {
                
                // Perform calculations
                logic();
                // Render the screen
                render();

                // If a second has passed since last calcFPS
                if(getTime() - lastTime >1) {
                // Do FPS-related activites
                calcFPS();
                }

                // Another frame has gone by, we're updating the counter.
                frameCounter++;

                // DEBUG
                if(SYNC) {
                    // SYNC
                    Display.sync(FRAME_RATE);
                }
            }

            // The window is not in the foreground
            // We allow other stuff to run and infrequently update.
            else {
                Thread.sleep(100);
                logic();

                // Only render if our display actually is visible and/or dirty
                if(Display.isVisible() || Display.isDirty()) {
                    render();
                }
            }
        }
    }

    /**
     * Initializes the OpenGL rendering
     */
    private void initgl() {

        // Get the dimensions for the current display-window
        int currentWidth = Display.getDisplayMode().getWidth();
        int currentHeight = Display.getDisplayMode().getHeight();

        // DEBUGGING
        if(DEBUG){
            System.out.println("Initializing OpenGL settings...");
        }

        // Select the projection matrix
        glMatrixMode(GL_PROJECTION);
        // Reset the projection matrix
        glLoadIdentity();
        
        // Calculate The Aspect Ratio Of The Window
        GLU.gluPerspective(
          45.0f,
          (float) currentWidth / (float) currentHeight,
          0.1f,
          100.0f);

        // Select the Modelview matrix
        glMatrixMode(GL_MODELVIEW);
        // Reset The Modelview Matrix
        glLoadIdentity();

        // Enable smooth shading
        glShadeModel(GL_SMOOTH);

        // Set the background to black
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);

        // Depth Buffer Setup
        glClearDepth(1.0f);

        // Enables Depth Testing
        glEnable(GL_DEPTH_TEST);

        // The Type Of Depth Test To Do
        glDepthFunc(GL_LEQUAL);

        // Nicest Perspective Calculations
        // This lowers performance but improves the quality
        glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
    }

    /**
     * Renders our display
     */
    private void render() {

        // DEBUGGING
        if(DEBUG){
            System.out.println("RENDERING...");
        }

        // Clear The Screen And The Depth Buffer
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        // Center the "pencil"
        glLoadIdentity();

        // Place the pointer at position (0,0,-5)
        glTranslatef(0f,0.0f,-5.0f);           

        // Rotate The Quad On The X axis
        glRotatef(rquad,1.0f,1.0f,1.0f);

        // Set The Color To Blue One Time Only
        glColor3f(0.5f,0.5f,1.0f);               
        
        glBegin(GL_QUADS);                       // Draw A Quad

            glColor3f(0.0f,1.0f,0.0f);     // Set The Color To Green
            glVertex3f( 1.0f, 1.0f,-1.0f); // Top Right Of The Quad (Top)
            glVertex3f(-1.0f, 1.0f,-1.0f); // Top Left Of The Quad (Top)
            glVertex3f(-1.0f, 1.0f, 1.0f); // Bottom Left Of The Quad (Top)
            glVertex3f( 1.0f, 1.0f, 1.0f); // Bottom Right Of The Quad (Top)

            glColor3f(1.0f,0.5f,0.0f);     // Set The Color To Orange
            glVertex3f( 1.0f,-1.0f, 1.0f); // Top Right Of The Quad (Bottom)
            glVertex3f(-1.0f,-1.0f, 1.0f); // Top Left Of The Quad (Bottom)
            glVertex3f(-1.0f,-1.0f,-1.0f); // Bottom Left Of The Quad (Bottom)
            glVertex3f( 1.0f,-1.0f,-1.0f); // Bottom Right Of The Quad (Bottom)

            glColor3f(1.0f,0.0f,0.0f);     // Set The Color To Red
            glVertex3f( 1.0f, 1.0f, 1.0f); // Top Right Of The Quad (Front)
            glVertex3f(-1.0f, 1.0f, 1.0f); // Top Left Of The Quad (Front)
            glVertex3f(-1.0f,-1.0f, 1.0f); // Bottom Left Of The Quad (Front)
            glVertex3f( 1.0f,-1.0f, 1.0f); // Bottom Right Of The Quad (Front)

            glColor3f(1.0f,1.0f,0.0f);     // Set The Color To Yellow
            glVertex3f( 1.0f,-1.0f,-1.0f); // Bottom Left Of The Quad (Back)
            glVertex3f(-1.0f,-1.0f,-1.0f); // Bottom Right Of The Quad (Back)
            glVertex3f(-1.0f, 1.0f,-1.0f); // Top Right Of The Quad (Back)
            glVertex3f( 1.0f, 1.0f,-1.0f); // Top Left Of The Quad (Back)

            glColor3f(0.0f,0.0f,1.0f);     // Set The Color To Blue
            glVertex3f(-1.0f, 1.0f, 1.0f); // Top Right Of The Quad (Left)
            glVertex3f(-1.0f, 1.0f,-1.0f); // Top Left Of The Quad (Left)
            glVertex3f(-1.0f,-1.0f,-1.0f); // Bottom Left Of The Quad (Left)
            glVertex3f(-1.0f,-1.0f, 1.0f); // Bottom Right Of The Quad (Left)

            glColor3f(1.0f,0.0f,1.0f);     // Set The Color To Violet
            glVertex3f( 1.0f, 1.0f,-1.0f); // Top Right Of The Quad (Right)
            glVertex3f( 1.0f, 1.0f, 1.0f); // Top Left Of The Quad (Right)
            glVertex3f( 1.0f,-1.0f, 1.0f); // Bottom Left Of The Quad (Right)
            glVertex3f( 1.0f,-1.0f,-1.0f); // Bottom Right Of The Quad (Right)
        glEnd(); 
    }

    /**
     * Performs the logic behind the scenes
     */
    private void logic() {

        // DEBUGGING
        if(DEBUG){
            System.out.println("PERFORMING LOGIC...");
        }
        
        // We check if a call has been made from the modulehandler to quit
        if(!SHELL_MODULE.getContinueBenchmarking()) {

            // DEBUGGING
            if(DEBUG){
                System.out.println("HANDLER REQUESTED STOP, EXITING LOOP...");
            }
            
            // If so, change the boolean var finished which will exit the game
            finished = !SHELL_MODULE.getContinueBenchmarking();
        }
        
        rquad-=0.10f;
    }

    /**
     * Returns the time of the high resolution timer in seconds.
     * @return time in seconds
     */
    private static double getTime() {

        // DEBUGGING
        if(DEBUG){
            System.out.println("GETTING THE TIME...");
        }

        return ((double) Sys.getTime()) / Sys.getTimerResolution();
    }


    /**
     * Calculates the current FPS
     */
    private void calcFPS() {

        // DEBUGGING
        if(DEBUG){
            System.out.println("CALCULATING FPS...");
        }

        // Calc the fps-value
        Long currentFPS = Math.round(frameCounter / (getTime() - lastTime));
        
        // Load the fps-value to the array if it is a valid number
        if(currentFPS != null && currentFPS != 0) {
            ACTUAL_FPS.add(currentFPS);
        }

        // Reset the frame counter
        frameCounter = 0;

        // Update the lastTime var
        lastTime = getTime();

        // DEBUGGING
        if(DEBUG){
            System.out.println("FPS CALCULATIONS COMPLETE...");
        }
    }
}